In [55]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
from sklearn.ensemble import RandomForestRegressor
from sklearn.feature_selection import RFE
from lime.lime_tabular import LimeTabularExplainer
import xgboost
from xgboost import XGBRegressor

PD3

Celem pracy domowej jest wyjaśnienie predykcji modelu na poziomie instacji. Modelowanie dotyczy predykcji prawdopodobieństwa, że dana osoba zostanie recydywistą (na podstawie zbioru COMPAS).

Wczytanie danych

In [249]:
raw_data = pd.read_csv("compas-scores-two-years.csv")

df = raw_data[["age", "c_charge_degree", "race", "age_cat", "score_text", "sex", "priors_count",
               "days_b_screening_arrest", "decile_score", "is_recid",
               "two_year_recid", "c_jail_in", "c_jail_out"]]
               

Przygotowanie danych

In [250]:
df = df[df["days_b_screening_arrest"] <= 30]
df = df[df["days_b_screening_arrest"] >= -30]
df = df[df["is_recid"] != -1]
df = df[df["c_charge_degree"] != "0"]
df = df[df["score_text"] != 'N/A']

df["prob_score"] = df.apply(lambda row: row['decile_score']/10, axis=1)
#df["length_of_stay"] = pd.to_numeric(pd.to_datetime(df["c_jail_out"]) - pd.to_datetime(df["c_jail_in"]))
df["length_of_stay"] = pd.to_datetime(df["c_jail_out"]) - pd.to_datetime(df["c_jail_in"])
df["length_of_stay_in_days"] = pd.to_numeric(df["length_of_stay"].dt.days)
df["factor_recid"], _ = pd.factorize(df["is_recid"])
df["factor_recid_two_years"], _ = pd.factorize(df["two_year_recid"])
df["factor_race"], _  = pd.factorize(df["race"])
df["factor_gender"], _ = pd.factorize(df["sex"])
df["factor_crime"], _ = pd.factorize(df["c_charge_degree"])
df["factor_score"], _ = pd.factorize(df["score_text"])
df["numeric_jail_in"] = pd.to_datetime(df["c_jail_in"])
#df["factor_age"],_ = pd.factorize(df["age_cat"])
df.shape
Out[250]:
(6172, 23)
In [251]:
regression_model_df = df[["age",
    "factor_recid",
    "factor_recid_two_years",
    "factor_race",
    "factor_gender",
    "factor_crime",
    "length_of_stay_in_days",
    "priors_count"]]

label = df["prob_score"]
In [267]:
X_train, X_test, y_train, y_test = train_test_split(regression_model_df, label, test_size=0.2, random_state=0)

Model: Random Forest

In [268]:
def classifier(model):
    model.fit(X_train, y_train)
    pred = model.predict(X_test)
    mse, mae, rscore = mean_squared_error(y_test, pred), mean_absolute_error(y_test, pred), r2_score(y_test, pred)
    print(f"MSE: {mse}, MAE: {mae}, R^2 score: {rscore}")
    return model, pred
In [269]:
model_rf, pred_rf = classifier(RandomForestRegressor(n_estimators = 100, random_state = 0))
MSE: 0.04702225049933595, MAE: 0.16775175755643368, R^2 score: 0.41362080157690895

Wyjaśnienie

Do wyjaśnienia predykcji na poziomie instancji i odpowiedzi na pytania dotyczące zmiennych wpływających na wartość predykcji użyję metody LIME.
In [270]:
lime_explainer = LimeTabularExplainer(X_train, 
                                        mode='regression',
                                        feature_names=X_train.columns,
                                        discretize_continuous=False,
                                        verbose=True)

Przykład 1

In [289]:
lime_explainer.explain_instance(X_test.iloc[10], model_rf.predict, num_features=6,num_samples=1000).show_in_notebook(show_table=True)
Intercept 0.5051290146015103
Prediction_local [0.37503302]
Right: 0.15599999999999978

Powyższy wykres przedstawia wyjaśnienie predykcji dla pojedynczej obserwacji o numerze 10. Dla tej obserwacji istotne są zmienne takie jak: wiek (age), liczba dokonanych wcześniej przestępstw (priors_count) oraz ilość pełnych dób spędzonych w więzieniu (length_of_stay_in_days), przy czym wiek wpływa na zmiejszenie p-stwa, że ta osoba zostanie recydywistą.

In [288]:
lime_explainer.explain_instance(X_test.iloc[10], model_rf.predict, num_features=6,num_samples=500).show_in_notebook(show_table=True)
Intercept 0.4984746569044749
Prediction_local [0.36935599]
Right: 0.15599999999999978
In [287]:
lime_explainer.explain_instance(X_test.iloc[10], model_rf.predict, num_features=6,num_samples=2000).show_in_notebook(show_table=True)
Intercept 0.5039651926099332
Prediction_local [0.36678896]
Right: 0.15599999999999978

Powyższe 2 wykresy przedstawiają wyjaśnienie predykcji dla tej samej obserwacji o numerze 10, ale ze zmienionym parametrem odpowiadającym za liczbę obserwacji wziętych do wyznaczenia Lime (num_samples) kolejno na: 500 i 2000. Jak widać wyjaśnienia dla tej obserwacji nie zmieniły się ze wględu na zmianę tego parametru, co może świadczyć o stabilności wyjaśnień.

Przykład 2

In [286]:
lime_explainer.explain_instance(X_test.iloc[998], model_rf.predict, num_features=6,num_samples=2000).show_in_notebook(show_table=True)
Intercept 0.49388215985366096
Prediction_local [0.37524981]
Right: 0.1210999999999998

Powyższy wykres przedstawia wyjaśnienie predykcji dla pojedynczej obserwacji o numerze 998. Dla tej obserwacji istotne są zmienne takie jak: wiek (age), liczba dokonanych wcześniej przestępstw (priors_count), ilość pełnych dób spędzonych w więzieniu (length_of_stay_in_days), oraz raca (factor_race) przy czym wiek oraz rasa (factor_race=Hispanic) wpływa na zmiejszenie p-stwa, że ta osoba zostanie recydywistą.

Przykład 3

In [284]:
lime_explainer.explain_instance(X_test.iloc[501], model_rf.predict, num_features=6,num_samples=2000).show_in_notebook(show_table=True)
Intercept 0.4911684666562717
Prediction_local [0.9138127]
Right: 0.8473333333333334

Powyższy wykres przedstawia wyjaśnienie predykcji dla pojedynczej obserwacji o numerze 501. W tym przypadku istotne są kolejno zmienne takie jak: liczba dokonanych wcześniej przestępstw (priors_count), wiek (age) oraz ilość pełnych dób spędzonych w więzieniu (length_of_stay_in_days), przy czym liczba dokonanych wcześniej przestępstw wpływa znacząco na zwiększenie p-stwa, że ta osoba zostanie recydywistą.

Warto zauważyć, że dla obserwacji 10 i 998, które są dosyć podobne, wyjaśnienia były bardzo zbliżone. Natomiast dla obserwacji 501, która miała na koncie sporą ilość dokonanych przestępstw, to wyjaśnienie nieco uległo zmianie, co również pokazuje stabilność wyjaśnień.

Model: XGBoost

Powyższy wykres przedstawia wyjaśnienie predykcji dla pojedynczej obserwacji o numerze 15. Liczba dokonanych wcześniej przestępstw (priorscount), wiek (age) oraz długość pobytu w więzieniu (lengthofstay) zwiększają p-stwo bycia recydywistą, zaś zmienna dotycząca płci (factor_gender) nieco zmniejszają to p-stwo. Ostatecznie predykcja wynosi 0.839.

In [277]:
xgb = XGBRegressor(objective='reg:logistic')
xgb.fit(X_train.values, y_train.values)
Out[277]:
XGBRegressor(base_score=0.5, booster=None, colsample_bylevel=1,
             colsample_bynode=1, colsample_bytree=1, gamma=0, gpu_id=-1,
             importance_type='gain', interaction_constraints=None,
             learning_rate=0.300000012, max_delta_step=0, max_depth=6,
             min_child_weight=1, missing=nan, monotone_constraints=None,
             n_estimators=100, n_jobs=0, num_parallel_tree=1,
             objective='reg:logistic', random_state=0, reg_alpha=0,
             reg_lambda=1, scale_pos_weight=1, subsample=1, tree_method=None,
             validate_parameters=False, verbosity=None)
In [278]:
y_pred_train = xgb.predict(X_train.values)
y_pred_test = xgb.predict(X_test.values)
mse, mae, rscore = mean_squared_error(y_test, y_pred_test), mean_absolute_error(y_test, y_pred_test), r2_score(y_test, y_pred_test)
print(f"MSE: {mse}, MAE: {mae}, R^2 score: {rscore}")
MSE: 0.0435327264103238, MAE: 0.16140307067135568, R^2 score: 0.4571360377994268

Wyjaśnienie

In [285]:
lime_explainer.explain_instance(X_test.iloc[501], xgb.predict, num_features=6,num_samples=2000).show_in_notebook(show_table=True)
Intercept 0.48660584498021403
Prediction_local [0.91897168]
Right: 0.8700018

Powyżej przedstawiam obserwację o numerze 501, która ma wysoką wartość predykcji w przypadku obu modeli. Wyjaśnienie wartości tej predykcji w przypadku modelu Random Forest i XGBoost różni się nieznacznie. W wyjaśnieniu modelu XGBoost pojawiają się zmienne factor_gender (płeć), która nie pojawiły się w wyjaśnieniu predykcji modelu Random Forest dla tej obserwacji. Z kolei w modelu Random Forest istotna (choć w niewielkim stopniu) była zmienna factor_race (dot. rasy).